package org.fit.pdfdom;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.pdmodel.common.function.PDFunction;
import org.apache.pdfbox.pdmodel.common.function.PDFunctionTypeIdentity;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;

/**
 * A Paint which applies a soft mask to an underlying Paint.
 *
 * @author Petr Slaby
 * @author John Hewson
 * @author Matthias Bläsing
 * @author Tilman Hausherr
 */
@Slf4j
class SoftMask implements Paint {

    private static final Log LOG = LogFactory.getLog(SoftMask.class);

    private static final ColorModel ARGB_COLOR_MODEL =
            new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getColorModel();

    private final Paint paint;
    private final BufferedImage mask;
    private final Rectangle2D bboxDevice;
    private int bc = 0;
    private final PDFunction transferFunction;

    /**
     * Creates a new soft mask paint.
     *
     * @param paint            underlying paint.
     * @param mask             soft mask
     * @param bboxDevice       bbox of the soft mask in the underlying Graphics2D device space
     * @param backdropColor    the color to be used outside the transparency group’s bounding box; if
     *                         null, black will be used.
     * @param transferFunction the transfer function, may be null.
     */
    SoftMask(Paint paint, BufferedImage mask, Rectangle2D bboxDevice, PDColor backdropColor, PDFunction transferFunction) {
        this.paint = paint;
        this.mask = mask;
        this.bboxDevice = bboxDevice;
        if (transferFunction instanceof PDFunctionTypeIdentity) {
            this.transferFunction = null;
        } else {
            this.transferFunction = transferFunction;
        }
        if (backdropColor != null) {
            try {
                Color color = new Color(backdropColor.toRGB());
                // http://stackoverflow.com/a/25463098/535646
                bc = (299 * color.getRed() + 587 * color.getGreen() + 114 * color.getBlue()) / 1000;
            } catch (IOException ex) {
                // keep default
                LOG.debug("Couldn't convert backdropColor to RGB - keeping default", ex);
            }
        }
    }

    @Override
    public PaintContext createContext(ColorModel cm, Rectangle deviceBounds,
                                      Rectangle2D userBounds, AffineTransform xform,
                                      RenderingHints hints) {
        PaintContext ctx = paint.createContext(cm, deviceBounds, userBounds, xform, hints);
        return new SoftMask.SoftPaintContext(ctx);
    }

    @Override
    public int getTransparency() {
        return TRANSLUCENT;
    }

    private class SoftPaintContext implements PaintContext {
        private final PaintContext context;

        SoftPaintContext(PaintContext context) {
            this.context = context;
        }

        @Override
        public ColorModel getColorModel() {
            return ARGB_COLOR_MODEL;
        }

        @Override
        public Raster getRaster(int x1, int y1, int w, int h) {
            Raster raster = context.getRaster(x1, y1, w, h);
            ColorModel rasterCM = context.getColorModel();
            float[] input = null;
            Float[] map = null;

            if (transferFunction != null) {
                map = new Float[256];
                input = new float[1];
            }

            // buffer
            WritableRaster output = getColorModel().createCompatibleWritableRaster(w, h);

            // the soft mask has its own bbox
            x1 = x1 - (int) bboxDevice.getX();
            y1 = y1 - (int) bboxDevice.getY();

            int[] gray = new int[4];
            Object pixelInput = null;
            int[] pixelOutput = new int[4];
            for (int y = 0; y < h; y++) {
                for (int x = 0; x < w; x++) {
                    pixelInput = raster.getDataElements(x, y, pixelInput);

                    pixelOutput[0] = rasterCM.getRed(pixelInput);
                    pixelOutput[1] = rasterCM.getGreen(pixelInput);
                    pixelOutput[2] = rasterCM.getBlue(pixelInput);
                    pixelOutput[3] = rasterCM.getAlpha(pixelInput);

                    // get the alpha value from the gray mask, if within mask bounds
                    gray[0] = 0;
                    if (x1 + x >= 0 && y1 + y >= 0 && x1 + x < mask.getWidth() && y1 + y < mask.getHeight()) {
                        mask.getRaster().getPixel(x1 + x, y1 + y, gray);
                        int g = gray[0];
                        if (transferFunction != null) {
                            // apply transfer function
                            try {
                                if (map[g] != null) {
                                    // was calculated before
                                    pixelOutput[3] = Math.round(pixelOutput[3] * map[g]);
                                } else {
                                    // calculate and store in map
                                    input[0] = g / 255f;
                                    float f = transferFunction.eval(input)[0];
                                    map[g] = f;
                                    pixelOutput[3] = Math.round(pixelOutput[3] * f);
                                }
                            } catch (IOException ex) {
                                // ignore exception, treat as outside
                                log.debug("Couldn't apply transferFunction - treating as outside", ex);
                                pixelOutput[3] = Math.round(pixelOutput[3] * (bc / 255f));
                            }
                        } else {
                            pixelOutput[3] = Math.round(pixelOutput[3] * (g / 255f));
                        }
                    } else {
                        pixelOutput[3] = Math.round(pixelOutput[3] * (bc / 255f));
                    }
                    output.setPixel(x, y, pixelOutput);
                }
            }

            return output;
        }

        @Override
        public void dispose() {
        }
    }
}